package weeny.recycle;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Insets;
import java.awt.Shape;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.BorderFactory;

import prefuse.Constants;
import prefuse.Display;
import prefuse.action.Action;
import prefuse.action.ActionList;
import prefuse.action.RepaintAction;
import prefuse.action.layout.AxisLabelLayout;
import prefuse.action.layout.AxisLayout;
import prefuse.controls.ControlAdapter;
import prefuse.data.Table;
import prefuse.data.Tuple;
import prefuse.data.query.NumberRangeModel;
import prefuse.render.AxisRenderer;
import prefuse.render.DefaultRendererFactory;
import prefuse.render.EdgeRenderer;
import prefuse.render.PolygonRenderer;
import prefuse.render.Renderer;
import prefuse.render.ShapeRenderer;
import prefuse.render.AbstractShapeRenderer;
import prefuse.util.ColorLib;
import prefuse.util.DataLib;
import prefuse.util.StrokeLib;
import prefuse.util.ui.UILib;
import prefuse.visual.AggregateItem;
import prefuse.visual.AggregateTable;
import prefuse.visual.EdgeItem;
import prefuse.visual.VisualItem;
import prefuse.visual.VisualTable;
import prefuse.visual.expression.InGroupPredicate;
import prefuse.visual.sort.ItemSorter;
import weeny.kdtree.KdTreeUtil;
import weeny.pre.display.BaseUIDisplay;
import weeny.pre.display.MoreVisualization;
import weeny.pre.util.Geom;
import weeny.pre.util.IntColor;
import weeny.pre.util.Pre;

public class CurveDisplay extends BaseUIDisplay {    
    private AggregateTable m_at = null;
    public static String PNT_GROUP = "point";
    public static String MAIN_CURVE = "main_curve_group";
    
    private static String AT_GROUP  = "at_Group";
    private static String BAR_GROUP = "bar_group";
    private static String BOX_GROUP = "box_group";
    private static String MAK_GROUP = "mark_group";
    private static String AREA_GROUP = "area_group";
    private static String XLABEL_GROUP = "xlab";
    private static String YLABEL_GROUP = "ylab";
    
    private static String CURVE_FIELD = "curve_field";
    
    private Rectangle2D m_dataB = new Rectangle2D.Double();
    private Rectangle2D m_xlabB = new Rectangle2D.Double();
    private Rectangle2D m_ylabB = new Rectangle2D.Double();
    
    private static int BAR_COL = IntColor.CATEGORY_5[4];
    private static int PNT_COL = IntColor.CATEGORY_5[1];
    
//    private static int RED = Pre.category5[0];
    private static int GREEN = ColorLib.rgb(136, 203, 127);
//    private static int BLUE = Pre.category5[3];
    
    private static int BOX_COL = IntColor.CATEGORY_5[3];
    private static int MAK_COL = IntColor.CATEGORY_5[1];
    
    private EdgeItem activeBar = null;
    @SuppressWarnings("deprecation")
	private MarkerHelper mark = null;

    private String xField = null;
    private String yField = null;
    private List<Table> curveTableList = null;
    
    PolygonRenderer boxRenderer = new PolygonRenderer(){
    	Rectangle2D rect = new Rectangle2D.Double();
    	@SuppressWarnings("deprecation")
		@Override
    	protected Shape getRawShape(VisualItem item) {
    		setClosePath(true);
    		rect.setFrame(item.getStartX(), m_dataB.getMinY(), 0, 0);
    		rect.add(item.getStartX(), m_dataB.getMaxY());
    		rect.add(item.getEndX(), m_dataB.getMinY());
    		item.set(PolygonRenderer.POLYGON, MarkerHelper.toPolyArray(rect));
    		return super.getRawShape(item);
    	}
    };
    EdgeRenderer barRenderer = new EdgeRenderer(){
    	@Override
    	protected Shape getRawShape(VisualItem item) {
    		setArrowType(Constants.EDGE_ARROW_NONE);
    		EdgeItem edge = (EdgeItem) item;
    		Pre.setY(edge.getSourceItem(), m_dataB.getMinY());
    		Pre.setY(edge.getTargetItem(), m_dataB.getMaxY());
    		return super.getRawShape(item);
    	}
    };
    ControlAdapter activeBarMoveControl = new ControlAdapter(){
    	private Point2D abs = new Point2D.Double();
    	@Override
    	public void mouseMoved(MouseEvent e) {
    		getAbsoluteCoordinate(e.getPoint(), abs);
    		if (!m_dataB.contains(abs)) {
    			if (abs.getX()>m_dataB.getMaxX()) {
					abs.setLocation(m_dataB.getMaxX(), 0);
				}
    			else if (abs.getX()<m_dataB.getMinX()){
    				abs.setLocation(m_dataB.getMinX(), 0);
    			}
			}
    		Pre.setX(activeBar.getSourceItem(), abs.getX());
    		Pre.setX(activeBar.getTargetItem(), abs.getX());

    		m_vis.setVisible(PNT_GROUP, null, false);
    		List list = kdtree.nearestList(activeBar);
    		for (Object object : list) {
				((VisualItem) object).setVisible(true);
			}
    		m_vis.repaint();
    	}
    	@Override
    	public void itemMoved(VisualItem item, MouseEvent e) {
    		mouseMoved(e);
    	}
    };
    
    private boolean canMarkBars = false;
    public void enableBarMarker(){
    	if (canMarkBars) {
    		return;
		}
    	canMarkBars = true;
    	m_vis.addFocusGroup(MAK_GROUP);
    	ControlAdapter markBarControl = new ControlAdapter(){
    		private Point2D abs = new Point2D.Double();
        	@Override
        	public void itemEntered(VisualItem item, MouseEvent e) {
        		if (item == activeBar) {
        			return;
        		}
        		if (item.isInGroup(MAK_GROUP)){
        			Display d = (Display)e.getSource();
        	        d.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
				}
        	}
        	@Override
        	public void itemExited(VisualItem item, MouseEvent e) {
    			Display d = (Display)e.getSource();
    	        d.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        	}
        	@SuppressWarnings("deprecation")
			@Override
        	public void itemClicked(VisualItem item, MouseEvent e) {
        		if (item == activeBar) {
        				mouseClicked(e);
        		}
        		else if (item.isInGroup(BAR_GROUP)){
        			m_vis.getFocusGroup(BAR_GROUP).removeTuple(item);
        			m_vis.getFocusGroup(MAK_GROUP).removeTuple(item);
        			mark.removeEdge(item);
        			m_vis.repaint();
        		}
        	}
        	@Override
        	public void mouseClicked(MouseEvent e) {
        		getAbsoluteCoordinate(e.getPoint(), abs);
        		if (!m_dataB.contains(abs)) {
        			if (abs.getX()>m_dataB.getMaxX()) {
						abs.setLocation(m_dataB.getMaxX(), 0);
					}
        			else if (abs.getX()<m_dataB.getMinX()){
        				abs.setLocation(m_dataB.getMinX(), 0);
        			}
				}
        		@SuppressWarnings("deprecation")
				EdgeItem edge = mark.markEdge(BAR_GROUP, Geom.PNT_ZERO, Geom.PNT_ZERO, MAK_COL);
        		m_vis.getFocusGroup(MAK_GROUP).addTuple(edge);
        		edge.setStroke(StrokeLib.getStroke(4));
        		Pre.setX(edge.getSourceItem(), abs.getX());
        		Pre.setX(edge.getTargetItem(), abs.getX());
        		m_vis.repaint();
        	}
    	};
    	addControlListener(markBarControl);
    }
    
    private boolean canMarkBox = false;
    public void enableBoxMarker(){
    	if (canMarkBox) {
    		return;
    	}
    	canMarkBox = true;
    	m_vis.addFocusGroup(BOX_GROUP);
    	ControlAdapter markBoxControl = new ControlAdapter(){
    		private Point2D abs = new Point2D.Double();
    		private VisualItem currentBox = null;
    		private VisualItem newBox(){
    			@SuppressWarnings("deprecation")
				VisualItem box = mark.markBox(BOX_GROUP, new Rectangle2D.Double(), BOX_COL, 1);
    			box.setFillColor(ColorLib.setAlpha(BOX_COL, 128));
    			return box;
    		}
    		@Override
    		public void mouseDragged(MouseEvent e) {
    			if (currentBox == null){
    				currentBox = newBox();
    			}
    			currentBox.setStartX(activeBar.getSourceItem().getX());
    			getAbsoluteCoordinate(e.getPoint(), abs);
    			currentBox.setEndX(abs.getX());
    			m_vis.repaint();
    		}
    		@Override
    		public void itemReleased(VisualItem item, MouseEvent e) {
    			mouseReleased(e);
    		}
    		@Override
    		public void mouseReleased(MouseEvent e) {
    			currentBox = null;
    		}
    		@Override
    		public void itemDragged(VisualItem item, MouseEvent e) {
    			mouseDragged(e);
    		}

    		@Override
    		public void itemEntered(VisualItem item, MouseEvent e) {
    			if (item == activeBar) {
    				return;
    			}
    			if (item.isInGroup(BOX_GROUP)){
    				Display d = (Display)e.getSource();
    				d.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
    			}
    		}
    		@Override
    		public void itemExited(VisualItem item, MouseEvent e) {
    			Display d = (Display)e.getSource();
    			d.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    		}
    		@SuppressWarnings("deprecation")
			@Override
    		public void itemClicked(VisualItem item, MouseEvent e) {
    			if (item.isInGroup(BOX_GROUP)){
    				m_vis.getFocusGroup(BOX_GROUP).removeTuple(item);
    				mark.removeTableItem(item);
    				damageReport();
    				m_vis.repaint();
    			}
    		}
    	};
    	addControlListener(markBoxControl);
    }
    
    private Map<VisualItem, List<VisualItem>> item2Area = new HashMap<VisualItem, List<VisualItem>>();
    @SuppressWarnings("deprecation")
	public void fillGap(int idx1, int idx2, int color){
    	List<VisualItem> list = new ArrayList<VisualItem>();
    	float[] array = new float[(2*curveTableList.get(idx1).getTupleCount()+6+3)*2];
		VisualTable vv = (VisualTable) m_vis.getVisualGroup(curveTableList.get(0).hashCode()+"");
		for(Iterator<VisualItem> it = vv.tuples(); it.hasNext();){
			VisualItem vi = it.next();
			if (vi.getRow() == 0) {
				list.add(vi);list.add(vi);list.add(vi);
			}
			list.add(vi);
			if (vi.getRow() == vv.getTupleCount()-1) {
				list.add(vi);list.add(vi);list.add(vi);
			}
		}
		vv = (VisualTable) m_vis.getVisualGroup(curveTableList.get(idx2).hashCode()+"");
		for(int i = vv.getTupleCount()-1; i>=0; i--){
			VisualItem vi = vv.getItem(i);
			list.add(vi);
			if (vi.getRow() == 0) {
				list.add(vi);list.add(vi);list.add(vi);
			}
		}
		for(int i = 0; i<list.size(); i++){
			array[i*2+0] = (float) list.get(i).getX();
			array[i*2+1] = (float) list.get(i).getY();
		}
		VisualItem vi = mark.markPoly(AREA_GROUP, array, color, 1);
		vi.setFillColor(color);
		item2Area.put(vi, list);
    }
    @SuppressWarnings("deprecation")
	public CurveDisplay(final List<Table> tableList, String curveX, String curveY){
        super(new MoreVisualization());
        this.xField = curveX;
        this.yField = curveY;
        getVisualization().addFocusGroup(PNT_GROUP);
        m_vis.addFocusGroup(BAR_GROUP);
        mark = new MarkerHelper(m_vis, false);
        activeBar = mark.markEdge(BAR_GROUP, Geom.PNT_ZERO, Geom.PNT_ZERO, BAR_COL);
        m_at = m_vis.addAggregates(AT_GROUP);

        final Display m_display = this;
        this.curveTableList = tableList;
        for(int i = 0; i<curveTableList.size(); i++){
        	Table table = curveTableList.get(i);
        	final VisualTable vt = m_vis.addTable(table.hashCode()+"", table);
        	vt.addColumn(CURVE_FIELD, AggregateItem.class);
            for(Iterator<Tuple> it = vt.tuples(); it.hasNext();){
            	getVisualization().getFocusGroup(PNT_GROUP).addTuple(it.next());
            }
            m_at.addColumns(PolygonRenderer.POLYGON_SCHEMA);
            AggregateItem curveLineItem = (AggregateItem) m_at.addItem();
            Pre.setStkCol(curveLineItem, IntColor.PALETTE[i]);
            if (curveTableList.size() == 1) {
            	Pre.setStkCol(curveLineItem, GREEN);
    		}
            curveLineItem.setStroke(Pre.roundStroke(4));
            for(Iterator<VisualItem> it = vt.tuples(); it.hasNext();){
            	VisualItem curveDotItem = it.next();
            	curveDotItem.set(CURVE_FIELD, curveLineItem);
            	curveLineItem.addItem(curveDotItem);
            }
		}
        
//        if (curveTableList.size() == 2) {
//			fillGap(0, 1, GREEN);
//		}
        AbstractShapeRenderer sr = new ShapeRenderer();

        PolygonRenderer pRenderer = new PolygonRenderer(Constants.POLY_TYPE_CURVE);
        pRenderer.setClosePath(false);
        Renderer arY = new AxisRenderer(Constants.RIGHT, Constants.TOP);
        Renderer arX = new AxisRenderer(Constants.CENTER, Constants.FAR_BOTTOM);
        DefaultRendererFactory drf = new DefaultRendererFactory(sr);
        drf.add(new InGroupPredicate(AREA_GROUP), new PolygonRenderer(Constants.EDGE_TYPE_CURVE));
        drf.add(new InGroupPredicate(AT_GROUP), pRenderer);
        drf.add(new InGroupPredicate(BAR_GROUP), barRenderer);
        drf.add(new InGroupPredicate(BOX_GROUP), boxRenderer);
        drf.add(new InGroupPredicate(XLABEL_GROUP), arX);
        drf.add(new InGroupPredicate(YLABEL_GROUP), arY);
        m_vis.setRendererFactory(drf);
        AxisLayout xaxis = new AxisLayout(PNT_GROUP, xField, Constants.X_AXIS, null); 
        AxisLayout yaxis = new AxisLayout(PNT_GROUP, yField, Constants.Y_AXIS, null);
        //yaxis.setScale(Constants.LOG_SCALE);
        double maxY = DataLib.max(m_vis.getFocusGroup(PNT_GROUP), yField).getDouble(yField);
        yaxis.setRangeModel(new NumberRangeModel(0, maxY, 0, maxY));
        xaxis.setLayoutBounds(m_dataB);
        
        AxisLabelLayout ylabels = new AxisLabelLayout("ylab", yaxis, m_ylabB);
        AxisLabelLayout xlabels = new AxisLabelLayout("xlab", xaxis, m_xlabB, 15);
        xlabels.setNumberFormat(new DecimalFormat("####"));
//        m_vis.putAction("xlabels", xlabels);
        
        m_vis.setValue(PNT_GROUP, null, VisualItem.FILLCOLOR, ColorLib.setAlpha(PNT_COL, 128));
        m_vis.setValue(PNT_GROUP, null, VisualItem.STROKECOLOR, PNT_COL);
        m_vis.setValue(PNT_GROUP, null, VisualItem.SHAPE, Constants.SHAPE_ELLIPSE);
        
        ActionList update = new ActionList();
        update.add(xaxis);
        update.add(yaxis);
        update.add(ylabels);
        update.add(xlabels);
        update.add(new Action() {
			@Override
			public void run(double frac) {
				//update all the curve shapes
				for (Iterator<AggregateItem> agIt = m_vis.items(AT_GROUP); agIt.hasNext();){
					AggregateItem item = agIt.next();
            		item.setValidated(false);
            		AggregateItem aItem = (AggregateItem) item;
            		ArrayList<VisualItem> items = new ArrayList<VisualItem>();
            		for(Iterator<VisualItem> it = aItem.items(); it.hasNext();){
            			items.add(it.next());
            		}
            		Collections.sort(items, new Comparator<VisualItem>(){
    					public int compare(VisualItem o1, VisualItem o2) {
    						return (int) Math.signum(o1.getX()-o2.getX());
    					}
            		});
            		float[] array = (float[]) item.get(PolygonRenderer.POLYGON);
            		if (array == null || array.length != items.size()*2) {
    					array = new float[items.size()*2];
    				}
            		for(int i = 0; i<items.size(); i++){
            			array[i*2+0] = (float) items.get(i).getX();
            			array[i*2+1] = (float) items.get(i).getY();
            		}
            		item.set(PolygonRenderer.POLYGON, array);
				}
				
				Iterator<VisualItem> pntIter = m_vis.items(PNT_GROUP);
		    	kdtree = new KdTreeUtil(){
		    		@Override
		    		protected double[] toArray(Object node) {
		    			if (node instanceof Double) {
							Double new_name = (Double) node;
							return new double[]{new_name};
						}
		    			else if (node instanceof EdgeItem){
		    				return new double[]{((EdgeItem) node).getSourceItem().getX()};
		    			}
		    			return new double[]{((VisualItem) node).getX()};
		    		}
		    	};
		    	kdtree.addIterator(pntIter, true);
		    	for(VisualItem item : item2Area.keySet()){
		    		float[] array = (float[]) item.get(PolygonRenderer.POLYGON);
		    		List<VisualItem> list = item2Area.get(item);
		    		for(int i = 0; i<list.size(); i++){
		    			array[i*2+0] = (float) list.get(i).getX();
		    			array[i*2+1] = (float) list.get(i).getY();
		    		}
		    		item.setValidated(false);
		    	}
			}
		});
        update.add(new RepaintAction());
        m_vis.putAction("update", update);
        
        m_vis.setVisible(PNT_GROUP, null, false);
        
        m_display.addControlListener(activeBarMoveControl);
        m_display.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
        m_display.setSize(1500,450);
        m_display.setHighQuality(true);
        m_display.addComponentListener(new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                displayLayout();
            }
        });
        
        m_display.setItemSorter(new ItemSorter(){
        	@Override
        	public int score(VisualItem item) {
        		int score = super.score(item);
        		if (item.isInGroup(AREA_GROUP)) {
					score = 0;
				}
        		else if (item == activeBar) {
					score = 1;
				}
        		return score;
        	}
        });
        displayLayout();        
        UILib.setColor(this, ColorLib.getColor(255,255,255), Color.GRAY);
    }
    public void displayLayout() {
        Insets i = getInsets();
        int w = getWidth();
        int h = getHeight();
        int iw = i.left+i.right;
        int ih = i.top+i.bottom;
        int aw = 15;
        int ah = 15;
//      curveItem.setValidated(false);
        m_dataB.setRect(i.left, i.top, w-iw-aw, h-ih-ah);
        m_xlabB.setRect(i.left, h-ah-i.bottom, w-iw-aw, ah-10);
        m_ylabB.setRect(i.left, i.top, w-iw, h-ih-ah);
        
        m_vis.run("update");
    }
    private KdTreeUtil kdtree = null;

    public VisualItem activeCurveItem(){
    	return (VisualItem) kdtree.nearest(activeBar);
    }
    public Point2D activePnt(){
    	Point2D result = curvePnt(activeCurveItem());
    	System.out.println("[INFO] active Point is "+result);
    	return result;
    }
    public Set<Point2D> selectedPnt(){
    	if (!canMarkBars) {
			throw new IllegalStateException("can not mark bars");
		}
    	Set<Point2D> result = new HashSet<Point2D>();
    	Iterator<EdgeItem> verticalMarkerIter = m_vis.items(MAK_GROUP);
    	while (verticalMarkerIter.hasNext()) {
    		EdgeItem barMark = verticalMarkerIter.next();
    		System.out.println(barMark);
    		VisualItem item = (VisualItem) kdtree.nearest(barMark);
    		result.add(curvePnt(item));
    	}
    	return result;
    }
    public static class Range{
    	public final Point2D from;
    	public final Point2D to;
    	public Range(Point2D from, Point2D to) {
    		this.from = from;
    		this.to = to;
		}
    }
    public Set<Range> selectedRange(){
    	if (!canMarkBox) {
			throw new IllegalStateException("can not mark ranges");
		}
    	Set<Range> result = new HashSet<Range>();
    	Iterator<VisualItem> boxIter = m_vis.items(BOX_GROUP);
    	while (boxIter.hasNext()){
    		VisualItem box = boxIter.next();
    		VisualItem from = (VisualItem) kdtree.nearest(new Double(box.getStartX()));
    		VisualItem to   = (VisualItem) kdtree.nearest(new Double(box.getEndX()));
    		result.add(new Range(curvePnt(from), curvePnt(to)));
    	}
    	return result;
    }
    private Point2D curvePnt(VisualItem item){
    	return new Point2D.Double(item.getDouble(xField), item.getDouble(yField));
    }
}